package edu.hda.pt.client;

import java.util.HashMap;

import edu.hda.pt.utils.Datatype;
import edu.hda.pt.utils.Logger;
import edu.hda.pt.utils.TestMethod;
import edu.hda.pt.utils.TestObject;

/**
 * PTClient is the super-class for the clients. It contains of the
 * implementation of the test methods.
 * 
 * @author Marc Hammerton
 * 
 */
public abstract class PTClient implements Runnable {

	/**
	 * The ID of the message
	 */
	private static int messageId = 0;

	/**
	 * The client ID to distinguish the clients
	 */
	public String clientID;

	/**
	 * The test method to run, e.g. round trip time
	 */
	public static TestMethod test2run;

	/**
	 * The data type which is to be used in the test
	 */
	public static Datatype datatype;

	/**
	 * The data size which is to be used in the test
	 */
	public static int datasize;

	/**
	 * The size of the frames during the streaming test
	 */
	public static int stream_fs;

	/**
	 * The number of frames received during the streaming test
	 */
	private int numberOfFrames;

	/**
	 * A map which is used to verify if all responses during the test are
	 * received. Can be used for debuggin purposes.
	 */
	public HashMap<Integer, Boolean> checkResults;

	/**
	 * Takes the results of the test
	 */
	public long[] results;

	/**
	 * Default constructor, initializing checkResults
	 */
	public PTClient() {
		checkResults = new HashMap<Integer, Boolean>();
	}

	/*
	 * Client methods, implemented by the connection methods
	 */

	/**
	 * Connect to the server
	 */
	public abstract void connect();

	/**
	 * Disconnect from the server
	 */
	public abstract void disconnect();

	/**
	 * Send a String value to the server
	 * 
	 * @param messageId
	 *            - The id of the message
	 * @param text
	 *            - The String value to be sent
	 */
	protected abstract void sendString(int messageId, String text);

	/**
	 * Send an Integer value to the server
	 * 
	 * @param messageId
	 *            - The id of the message
	 * @param intPrim
	 *            - The Integer value to be sent
	 */
	protected abstract void sendInt(int messageId, int intPrim);

	/**
	 * Send a Long value to the server
	 * 
	 * @param messageId
	 *            - The id of the message
	 * @param longPrim
	 *            - The Long value to be sent
	 */
	protected abstract void sendLong(int messageId, long longPrim);

	/**
	 * Send a Boolean value to the server
	 * 
	 * @param messageId
	 *            - The id of the message
	 * @param boolPrim
	 *            - The Boolean value to be sent
	 */
	protected abstract void sendBool(int messageId, boolean boolPrim);

	/**
	 * Send an object to the server
	 * 
	 * @param messageId
	 *            - The id of the message
	 * @param testObject
	 *            - The object to be sent
	 */
	protected abstract void sendObject(int messageId, TestObject testObject);

	/**
	 * Send a Byte value to the server
	 * 
	 * @param messageId
	 *            - The id of the message
	 * @param bytes
	 *            - The Byte value
	 */
	protected abstract void sendBytes(int messageId, byte[] bytes);

	/**
	 * Send a request for a stream to the server
	 * 
	 * @param fs
	 *            - The size of the frames
	 */
	protected abstract void sendStreamingRequest(int fs);

	/**
	 * Request the server to stop the streaming
	 */
	protected abstract void terminateStreaming();

	/**
	 * To initialize the test, a certain number of requests are sent to the
	 * server without measuring.
	 */
	public void warmUp() {
		int id = 0;
		for (int i = 0; i < Config.WARM_UP_METHOD_CALLS; i++) {
			id = messageId++;
			switch (PTClient.datatype) {
				case SHORTSTRING:
					sendString(id, "string");
					break;
				case LONGSTRING:
					sendString(id, "string");
					break;
				case INTEGER:
					sendInt(id, 1);
					break;
				case LONG:
					sendLong(id, 1L);
					break;
				case BOOLEAN:
					sendBool(id, true);
					break;
				case OBJECT:
					sendObject(id, new TestObject());
					break;
				case BYTES:
					sendBytes(id, new byte[1]);
					break;
				default:
					break;
			}
		}
	}

	/**
	 * Method for checking if all requests are received
	 */
	public void checkReceivedMessages() {
		if (checkResults.containsValue(false)) {
			System.out
					.println("checkReceivedMessages -> not all messages received");
			System.out.println(checkResults.values());
		} else {
			System.out.println("checkReceivedMessages -> All good");
		}
	}

	/*
	 * Performance test methods
	 */

	/**
	 * Test method for measuring the round trip time
	 */
	public void performRoundTripTimeTest() {

		// completeResults.clear();
		// checkResults.clear();
		int id = 0;

		long rtt_start = 0L;
		long rtt = 0L;

		// Test sending String (long)
		results = new long[Config.METHOD_CALLS];
		for (int i = 0; i < Config.METHOD_CALLS; i++) {
			id = messageId++;
			// checkResults.put(id, false);
			switch (PTClient.datatype) {
				case SHORTSTRING:
					rtt_start = System.nanoTime();
					sendString(id, Config.SHORT_STRING);
					rtt = System.nanoTime();
					break;
				case LONGSTRING:
					rtt_start = System.nanoTime();
					sendString(id, Config.LONG_STRING);
					rtt = System.nanoTime();
					break;
				case INTEGER:
					rtt_start = System.nanoTime();
					sendInt(id, Config.INT_PRIM);
					rtt = System.nanoTime();
					break;
				case LONG:
					rtt_start = System.nanoTime();
					sendLong(id, Config.LONG_PRIM);
					rtt = System.nanoTime();
					break;
				case BOOLEAN:
					rtt_start = System.nanoTime();
					sendBool(id, Config.BOOL_PRIM);
					rtt = System.nanoTime();
					break;
				case OBJECT:
					rtt_start = System.nanoTime();
					sendObject(id, new TestObject());
					rtt = System.nanoTime();
					break;
				default:
					break;
			}

			rtt = rtt - rtt_start;
			results[i] = rtt;
		}

		// checkReceivedMessages();

	}

	/**
	 * Test method for measuring the throughput
	 */
	public void performThroughputTest() {

		// completeResults.clear();
		// checkResults.clear();
		int id = 0;

		long tp_start = 0L;
		long tp_elapsedTime = 0L;
		int tp = 0;

		// Test sending Strings (short)
		results = new long[1];
		tp_start = System.currentTimeMillis();
		tp_elapsedTime = 0L;
		tp = 0;
		while (tp_elapsedTime < Config.TIME_SPAN) {
			id = messageId++;

			switch (PTClient.datatype) {
				case SHORTSTRING:
					sendString(id, Config.SHORT_STRING);
					break;
				case LONGSTRING:
					sendString(id, Config.LONG_STRING);
					break;
				case INTEGER:
					sendInt(id, Config.INT_PRIM);
					break;
				case LONG:
					sendLong(id, Config.LONG_PRIM);
					break;
				case BOOLEAN:
					sendBool(id, Config.BOOL_PRIM);
					break;
				case OBJECT:
					sendObject(id, new TestObject());
					break;
				default:
					break;
			}

			tp_elapsedTime = System.currentTimeMillis() - tp_start;
			tp++;
		}
		results[0] = tp;
	}

	/**
	 * Test method for measuring the data throughput
	 */
	public void performDataThroughputTest() {

		long dtt_start = 0L;
		long dtt = 0L;

		int id = 0;

		results = new long[Config.METHOD_CALLS];
		for (int i = 0; i < Config.METHOD_CALLS; i++) {
			byte[] testBytes = new byte[PTClient.datasize];
			id = messageId++;

			dtt_start = System.nanoTime();
			sendBytes(id, testBytes);
			dtt = System.nanoTime();
			dtt = dtt - dtt_start;

			results[i] = dtt;
		}

	}

	/**
	 * Test method for performing the streaming test
	 */
	public void performStreamingTest() {
		results = new long[1];
		numberOfFrames = 0;

		long timeRemaining = Config.STREAM_TIMESPAN;
		long warmUpRemaining = Config.STREAM_WARMUP;

		// Wait for start
		try {
			synchronized (this) {
				this.wait(2 * timeRemaining);
			}
		} catch (InterruptedException e) {
			e.printStackTrace();
		}

		long time = System.currentTimeMillis();
		long startTime = time;

		// Run warmup
		while (warmUpRemaining > 0) {
			try {
				synchronized (this) {
					this.wait(timeRemaining);
				}
			} catch (InterruptedException e) {
				e.printStackTrace();
			}

			time = System.currentTimeMillis();
			warmUpRemaining = Config.STREAM_WARMUP - (time - startTime);
		}

		// Run test
		time = System.currentTimeMillis();
		startTime = time;
		while (timeRemaining > 0) {
			numberOfFrames++;

			try {
				synchronized (this) {
					this.wait(timeRemaining);
				}
			} catch (InterruptedException e) {
				e.printStackTrace();
			}

			time = System.currentTimeMillis();
			timeRemaining = Config.STREAM_TIMESPAN - (time - startTime);
		}

		results[0] = numberOfFrames;
	}

	/**
	 * Test method for performing the synchronous CPU utilization and memory
	 * usage test
	 */
	public void performCPUAndMemorySyncTest() {

		int id = 0;

		long cpum_start = 0L;
		long cpum_elapsedTime = 0L;

		cpum_start = System.currentTimeMillis();

		while (cpum_elapsedTime < Config.CPU_AND_MEM_TEST_TIME) {
			byte[] testBytes = new byte[PTClient.datasize];
			id = messageId++;

			sendBytes(id, testBytes);

			cpum_elapsedTime = System.currentTimeMillis() - cpum_start;
		}
	}

	/**
	 * Test method for performing the asynchronous CPU utilization and memory
	 * usage test
	 */
	public void performCPUAndMemoryAsyncTest() {
		long timeRemaining = Config.CPU_AND_MEM_TEST_TIME;

		// Wait for start
		try {
			synchronized (this) {
				this.wait(2 * timeRemaining);
			}
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}

		long time = System.currentTimeMillis();
		long startTime = time;

		// Run test
		time = System.currentTimeMillis();
		startTime = time;
		while (timeRemaining > 0) {

			try {
				synchronized (this) {
					this.wait(timeRemaining);
				}
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}

			time = System.currentTimeMillis();
			timeRemaining = Config.STREAM_TIMESPAN - (time - startTime);
		}

	}

	/**
	 * Debugging method - can be used to measure several interim times
	 */
	public void testTime() {
		int id;
		// checkResults.clear();
		for (int i = 0; i < Config.METHOD_CALLS; i++) {
			id = messageId++;

			switch (PTClient.datatype) {
				case SHORTSTRING:
					Logger.logTestTimes(this.clientID, 0, System.nanoTime());
					sendString(id, Config.SHORT_STRING);
					Logger.logTestTimes(this.clientID, 9, System.nanoTime());
					break;
				case LONGSTRING:
					Logger.logTestTimes(this.clientID, 0, System.nanoTime());
					sendString(id, Config.LONG_STRING);
					Logger.logTestTimes(this.clientID, 9, System.nanoTime());
					break;
				case INTEGER:
					// checkResults.put(id, false);
					Logger.logTestTimes(this.clientID, 0, System.nanoTime());
					sendInt(id, Config.INT_PRIM);
					Logger.logTestTimes(this.clientID, 9, System.nanoTime());
					break;
				case LONG:
					Logger.logTestTimes(this.clientID, 0, System.nanoTime());
					sendLong(id, Config.LONG_PRIM);
					Logger.logTestTimes(this.clientID, 9, System.nanoTime());
					break;
				case BOOLEAN:
					Logger.logTestTimes(this.clientID, 0, System.nanoTime());
					sendBool(id, Config.BOOL_PRIM);
					Logger.logTestTimes(this.clientID, 9, System.nanoTime());
					break;
				case OBJECT:
					Logger.logTestTimes(this.clientID, 0, System.nanoTime());
					sendObject(id, new TestObject());
					Logger.logTestTimes(this.clientID, 9, System.nanoTime());
					break;
				default:
					break;
			}

		}

		// this.checkReceivedMessages();
	}

	/**
	 * Run the test. First connect to the server, then run the actual test and
	 * finally disconnect from the server.
	 * 
	 * @see java.lang.Runnable#run()
	 */
	@Override
	public void run() {
		this.connect();

		switch (PTClient.test2run) {
			case CPUMSYNC:
				this.performCPUAndMemorySyncTest();
				break;
			case CPUMASYNC:
				sendStreamingRequest(PTClient.stream_fs);
				this.performCPUAndMemoryAsyncTest();
				terminateStreaming();
				break;
			case RTT:
				this.warmUp();
				this.performRoundTripTimeTest();
				break;
			case TP:
				this.warmUp();
				this.performThroughputTest();
				break;
			case DTP:
				this.warmUp();
				this.performDataThroughputTest();
				break;
			case TESTTIME:
				this.warmUp();
				this.testTime();
				break;
			case STREAMING:
				sendStreamingRequest(PTClient.stream_fs);
				performStreamingTest();
				terminateStreaming();
				break;
			default:
				System.out.println("Please choose a test to run!");
				break;
		}

		this.disconnect();
	}

}
